<?php

namespace mongrove;

use IteratorAggregate;
use ArrayAccess;
use ArrayIterator;
use Traversable;

/**
 *
 * The ListField represents a list of values, possibly from the same type. All
 * actions performed at the ListField are recorded for the save action of its
 * containing Record and are compacted and played back in order for all modifications
 * to be saved.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
class ListField extends AbstractField implements IteratorAggregate, ArrayAccess, CompositeField {

    protected $list;

    const POP_FIRST	= -1;
    const POP_LAST	= 1;

    const MUT_ADD = 0x01;
    const MUT_REM = 0x02;
    const MUT_UNS = 0x03;
    const MUT_PFR = 0x04;
    const MUT_PBK = 0x05;

    protected $add = array();
    protected $remove = array();
    protected $unset = array();

    protected $mutations = array();

    protected $field;

    /**
     * Define a new ListField with an optional array of default entries.

     * @param array $default The default values in this field
     */
    public function __construct(array $default = array()) {
        parent :: __construct();

        $this->field = new SimpleField();

        if(isset($default)) {
            $this->setValue($default);
        }
    }

    /**
     * Wrap a value in the container Field
     *
     * @param mixed $value
     * @return Field wrapping field
     */
    protected function toField($value) {
        // TODO evaluate use of clone
        $field = clone $this->field;
        $field->setValue($value);
        //        $field->clean();

        return $field;
    }

    /**
     * Set the required Field type of the ListField
     *
     * @param Field $field
     *
     * @return ListField
     */
    public function setField(Field $field) {
        $this->field = $field;

        return $this;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getValue()
     */
    public function getValue() {
        return $this;
    }

    /**
    *
    * Check whether the CompositeField has a Field with the given name.
    *
    * @param $name The name of the Field to check
    *
    * @return boolean True when the record has a Field of this name
    */
    public function hasField($name) {
        if($this->field instanceof CompositeField) {
            return $this->field->hasField($name);
        }
        return false;
    }
    
    /**
     * Get the Field with the given name.
     *
     * @param string $name
     *
     * @return Field|null
     */
    public function getField($name) {
        if($this->field instanceof CompositeField) {
            return $this->field->getField($name);
        }
        return null;
    }
    
    /**
     * (non-PHPdoc)
     * @see src/mongrove.AbstractField::setValueImpl()
     */
    protected function setValueImpl($value) {
        if($this->list === $value) {
            return false;
        }

        if(!(is_array($value) || ($value instanceof Traversable))) {
            throw new \Exception("{$value} is not of a traversable type");
        }

        $this->list = array();

        foreach($value as $_ => $containedValue) {
            $this->list[] = $this->toField($containedValue);
        }

        $this->add = array();
        $this->remove = array();
        $this->pop = null;

        $this->_state |= self :: STATE_NEW;
        // TODO, mark as dirty, effective full dehydrate of lower elements

        return true;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::hydrate()
     */
    public function hydrate($value) {
        $this->list = array();

        foreach($value as $key => $fieldValue) {
            $field = clone $this->field;
            $field->hydrate($fieldValue);

            $this->list[$key] = $field;
        }
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::dehydrate()
     */
    public function dehydrate() {
        $result = array();

        foreach($this->list as $key => $value) {
            $result[$key] = $value->dehydrate();
        }

        return $result;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getMutations()
     */
    public function getMutations($path = null, $name = null) {
        $path === null ?: $path .= '.';

        /*
         * Require a full dehydrate on direct set
         */
        if($this->isNew()) {
            return array(array(Command :: OP_SET => array("{$path}{$name}" => $this->dehydrate())));
        }


        $key = $path ? $path . $name : $name;

        $mutations = array();

        $mutationGroups = array();

        { // Naive grouping of mutations
            $lastMutation = 0x00;
            $mutationGroup = null;

            reset($this->add);
            reset($this->remove);
            reset($this->unset);

            foreach($this->mutations as $mutation) {
                if($lastMutation !== $mutation) {
                    if($mutationGroup !== null) {
                        $mutationGroups[] = array($lastMutation, $mutationGroup);
                    }
                    $lastMutation = $mutation;
                    $mutationGroup = array();
                }

                switch($mutation) {
                    case self :: MUT_ADD :
                        $mutationGroup[] = current($this->add);next($this->add);
                        break;
                    case self :: MUT_PBK :
                        $mutationGroup[] = self :: MUT_PBK;
                        break;
                    case self :: MUT_PFR :
                        $mutationGroup[] = self :: MUT_PFR;
                        break;
                    case self :: MUT_REM :
                        $mutationGroup[] = current($this->remove);next($this->remove);
                        break;
                    case self :: MUT_UNS :
                        $mutationGroup[] = current($this->unset);next($this->unset);
                        break;
                }
            }

            if($mutationGroup !== null) {
                $mutationGroups[] = array($lastMutation, $mutationGroup);
            }
        }

        { // Compaction of mutation series
            foreach($mutationGroups as $mutationGroup) {
                list($mutation, $values) = $mutationGroup;

                switch($mutation) {
                    case self :: MUT_ADD : {
                        if(count($values) === 1) {
                            $mutations[] = array(Command::OP_PUSH => array($key => $values[0]));
                        } else {
                            $mutations[] = array(Command::OP_PUSH_ALL => array($key => $values));
                        }
                        break;
                    }
                    case self :: MUT_PBK : {
                        foreach($values as $_) {
                            $mutations[] = array(Command::OP_POP => array($key => self :: POP_LAST));
                        }
                        break;
                    }
                    case self :: MUT_PFR : {
                        foreach($values as $_) {
                            $mutations[] = array(Command::OP_POP => array($key => self :: POP_FIRST));
                        }
                        break;
                    }
                    case self :: MUT_REM : {
                        if(count($values) === 1) {
                            $mutations[] = array(Command::OP_PULL => array($key => $values[0]));
                        } else {
                            $mutations[] = array(Command::OP_PULL_ALL => array($key => $values));
                        }
                        break;
                    }
                    case self :: MUT_UNS : {
                        $unset = array();
                        foreach($values as $value) {
                            $unset["{$key}.{$value}"] = 1;
                        }
                        $mutations[] = array(Command :: OP_UNSET => $unset);
                        $mutations[] = array(Command :: OP_PULL => array($key => null));

                        break;
                    }
                }
            }
        }

        $finalMutations = array();

        { // Finally apply modified internal mutations
            foreach($this->list as $index => $field) {
                $setMutations = $field->getMutations($key, $index);

                foreach($setMutations as $cycle => $cycleMutations) {
                    if(!isset($finalMutations[$cycle])) {
                        $finalMutations[$cycle] = $cycleMutations;
                    } else {
                        $finalMutations[$cycle] = array_merge_recursive($finalMutations[$cycle], $cycleMutations);
                    }
                }
            }
        }

        foreach($finalMutations as $mutation) {
            array_push($mutations, $mutation);
        }

        return $mutations;

    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.AbstractField::clean()
     */
    public function clean() {
        parent :: clean();

        foreach($this->list as $field) {
            $field->clean();
        }

        $this->add = array();
        $this->remove = array();
        $this->unset = array();
        $this->mutations = array();
    }


    /**
     * Add a value to the list.
     *
     * @param mixed $value The value to be added to the list
     */
    public function add($value) {
        $value = $this->toField($value);

        $this->list[] = $value;

        $this->add[] = $value->dehydrate();
        $this->mutations[] = self :: MUT_ADD;

        $this->_state |= self :: STATE_DIRTY;
    }

    /**
     * Add a collection of values to the list.
     *
     * @param array|\Traversable $values The values to be added to the list
     */
    public function addAll($values) {
        if(!is_array($values) || !($values instanceof Traversable)) {
            throw new \Exception("Type mismatch, expected array or Traversable");
        }

        foreach($values as $value) {
            $this->add($value);
        }
    }

    /**
     * Remove all occurrences of the given value from the list.
     *
     * @param mixed $value The value to be removed from the list
     */
    public function remove($value) {
        $value = $this->toField($value);

        foreach($this->list as $key => $elem) {
            if($value->dehydrate() === $value->dehydrate()) {
                unset($this->list[$key]);
            }
        }

        $this->remove[] = $value->dehydrate();
        $this->mutations[] = self :: MUT_REM;

        $this->_state |= self :: STATE_DIRTY;
    }

    /**
     * Remove all occurrences of all given values from the list.
     *
     * @param array|\Traversable $values The collection of values which are to be removed
     */
    public function removeAll($values) {
        if(!is_array($values) || !($values instanceof Traversable)) {
            throw new \Exception("Type mismatch, expected array or Traversable");
        }

        foreach($values as $value) {
            $this->remove($value);
        }
    }

    /**
     * Remove and return the first element from the list.
     *
     * @return mixed The first value from the list or null if the list was empty
     */
    public function removeFirst() {


        $result = null;

        if(count($this->list) > 0) {
            $this->mutations[] = self :: MUT_PFR;
            $this->_state |= self :: STATE_DIRTY;
            $result = array_shift($this->list);
            $result = $result->dehydrate();
        }

        return $result;
    }

    /**
     * Remove and return the last element from the list.
     *
     * @return mixed The last value from the list or null if the list was empty
     */
    public function removeLast() {

        $result = null;

        if(count($this->list) > 0) {
            $this->mutations[] = self :: MUT_PBK;
            $this->_state |= self :: STATE_DIRTY;
            $result = array_pop($this->list);
            $result = $result->dehydrate();
        }

        return $result;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::rewriteQuery()
     */
    public function rewriteQuery(array $partialQuery) {

        // TODO enforce the type
        return $this->field->rewriteQuery($partialQuery); //$partialQuery;
    }

    /*
     * ArrayAccess method implementations
     */
    public function offsetExists($offset) {
        return isset($this->list[$offset]);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetGet()
     */
    public function offsetGet($offset) {
        $result = $this->list[$offset];

        return $result->getValue();
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetSet()
     */
    public function offsetSet($offset, $value) {
        if($offset === null) {
            $this->add($value);
        } else {
            $field = $this->list[$offset];
            $field->setValue($value);
        }
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetUnset()
     */
    public function offsetUnset($offset) {
        $this->mutations[] = self :: MUT_UNS;
        $this->unset[] = $offset;
        unset($this->list[$offset]);

        $this->list = array_values($this->list);
    }

    /**
     * (non-PHPdoc)
     * @see IteratorAggregate::getIterator()
     */
    public function getIterator() {
        return new ListFieldIterator($this->list);
    }

    /**
     * Allow lists of lists of lists (etc...)
     */
    public function __clone() {
        foreach($this->list as $index => $field) {
            $this->list[$index] = clone $field;
        }
    }
}

/**
 *
 * A modified ArrayIterator for smart iteration over the ListField.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
class ListFieldIterator extends \ArrayIterator {

    /**
     * (non-PHPdoc)
     * @see ArrayIterator::offsetGet()
     */
    public function offsetGet($index) {
        return parent :: offsetGet($index)->getValue();
    }

    /**
     * (non-PHPdoc)
     * @see ArrayIterator::current()
     */
    public function current() {
        return parent :: current()->getValue();
    }
}